Skip to content

feat(spp_hazard): CAP vocabulary severity and alert ingestion, with upgrade migration (re-land from #76)#274

Open
gonzalesedwin1123 wants to merge 8 commits into
19.0from
reland/hazard-cap
Open

feat(spp_hazard): CAP vocabulary severity and alert ingestion, with upgrade migration (re-land from #76)#274
gonzalesedwin1123 wants to merge 8 commits into
19.0from
reland/hazard-cap

Conversation

@gonzalesedwin1123

Copy link
Copy Markdown
Member

Re-lands the spp_hazard portion of reverted PR #76 (revert: #271), together with the dependent adaptations and the two fixes whose absence triggered the revert discussion.

Summary

  • spp.hazard.incident.severity (1-5 Selection) becomes severity_id, a spp.vocabulary.code Many2one on the CAP v1.2 severity vocabulary; severity_override on incident areas becomes severity_override_id likewise.
  • New CAP fields: urgency, certainty, message type, event, effective/expires; alert ingestion via create_incident_from_alert (creates hazard-zone geofences, auto-links intersecting areas); incident uuid.
  • CAP vocabulary data (severity, urgency, certainty, msg-type).
  • Dependent adaptations shipped together because they reference the new fields directly: spp_drims (effective_severity_id, numeric choropleth severity), spp_drims_sl_demo (scenario severity resolution), spp_hazard_programs (program view).

Added vs the original PR

  • Upgrade migration (spp_hazard 19.0.2.1.0): backfills severity_id/severity_override_id from the legacy Selection columns for databases upgrading from v19.0.2.0.x (release Biliran ships the old schema). Label-faithful mapping: 1→minor, 2→moderate, 3→severe (CAP defines severe as 'Significant threat'), 4→severe, 5→extreme. Idempotent; never overwrites values; unmapped values logged and left empty; no-op on fresh installs. Regression tests included.
  • Demo data fix: four incident-area records still wrote the removed severity_override field, breaking fresh installs with demo data; they now use severity_override_id vocabulary refs.

Verification

  • ./spp t spp_hazard: 87 passed, 0 failed (includes 5 new migration tests)
  • ./spp t spp_drims: 250/0, spp_drims_sl_demo: 8/0, spp_hazard_programs: 24/0

Comment thread spp_hazard/migrations/19.0.2.1.0/post-migration.py Fixed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request migrates hazard incident severity tracking from a hardcoded 1-5 Selection to a CAP v1.2 vocabulary code, introducing new CAP-aligned fields (urgency, certainty, message type, and event) and robust alert ingestion methods that automatically create or update incidents and geofences from GeoJSON alerts. It also includes a post-migration script to backfill legacy severity values and updates related tests, views, and demo generators across dependent modules. The review feedback highlights three important improvements: making the _parse_datetime_string helper robust against already-parsed datetime objects, wrapping the PostGIS intersection query in a savepoint to prevent transaction aborts on database errors, and restricting the migration's column existence check to the active schema to avoid false positives.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +22 to +37
def _parse_datetime_string(value):
"""Parse a datetime string in either ISO 8601 or Odoo format.

Handles both '2026-04-01T00:00:00Z' (ISO 8601) and
'2026-04-01 00:00:00' (Odoo Datetime format).

Returns:
datetime object
"""
# Replace 'Z' with '+00:00' for fromisoformat compatibility
normalized = value.replace("Z", "+00:00")
dt = datetime.fromisoformat(normalized)
# Odoo requires naive (UTC) datetimes
if dt.tzinfo is not None:
dt = dt.astimezone(UTC).replace(tzinfo=None)
return dt

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The helper function _parse_datetime_string assumes that value is always a string. However, in Odoo, datetime fields or values passed programmatically (e.g., in properties or context) can sometimes already be datetime objects. Calling .replace("Z", "+00:00") on a datetime object will raise a TypeError.\n\nTo make this helper robust and prevent potential runtime crashes, check if value is already a datetime instance before attempting string manipulation.

Suggested change
def _parse_datetime_string(value):
"""Parse a datetime string in either ISO 8601 or Odoo format.
Handles both '2026-04-01T00:00:00Z' (ISO 8601) and
'2026-04-01 00:00:00' (Odoo Datetime format).
Returns:
datetime object
"""
# Replace 'Z' with '+00:00' for fromisoformat compatibility
normalized = value.replace("Z", "+00:00")
dt = datetime.fromisoformat(normalized)
# Odoo requires naive (UTC) datetimes
if dt.tzinfo is not None:
dt = dt.astimezone(UTC).replace(tzinfo=None)
return dt
def _parse_datetime_string(value):
"""Parse a datetime string in either ISO 8601 or Odoo format.
Handles both '2026-04-01T00:00:00Z' (ISO 8601) and
'2026-04-01 00:00:00' (Odoo Datetime format). Also handles datetime objects.
Returns:
datetime object
"""
if isinstance(value, datetime):
dt = value
else:
# Replace 'Z' with '+00:00' for fromisoformat compatibility
normalized = value.replace("Z", "+00:00")
dt = datetime.fromisoformat(normalized)
# Odoo requires naive (UTC) datetimes
if dt.tzinfo is not None:
dt = dt.astimezone(UTC).replace(tzinfo=None)
return dt

Comment thread spp_hazard/models/hazard_incident.py Outdated
Comment on lines +528 to +553
self.env.cr.execute(
"""
SELECT id FROM spp_area
WHERE geo_polygon IS NOT NULL
AND ST_Intersects(
geo_polygon::geometry,
ST_SetSRID(ST_GeomFromGeoJSON(%s), 4326)
)
""",
(geojson_str,),
)
area_ids = [row[0] for row in self.env.cr.fetchall()]
if area_ids:
self.write({"area_ids": [Command.set(area_ids)]})
else:
_logger.info(
"No intersecting areas found for incident %s",
self.code,
)
except psycopg2.Error as e:
_logger.warning(
"Failed to link areas from geometry for incident %s: %s",
self.code,
e,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In PostgreSQL/psycopg2, any error occurring during a query execution (such as an invalid GeoJSON format passed to ST_GeomFromGeoJSON) will abort the current transaction. Simply catching psycopg2.Error and logging a warning is insufficient because the transaction remains in an aborted state, causing all subsequent database operations in the same request to fail with InFailedSqlTransaction.\n\nTo safely handle potential database errors during geometry intersection and allow the transaction to continue, wrap the database execution inside a savepoint using with self.env.cr.savepoint():.

        try:
            with self.env.cr.savepoint():
                self.env.cr.execute(
                    """
                    SELECT id FROM spp_area
                    WHERE geo_polygon IS NOT NULL
                    AND ST_Intersects(
                        geo_polygon::geometry,
                        ST_SetSRID(ST_GeomFromGeoJSON(%s), 4326)
                    )
                    """,
                    (geojson_str,),
                )
                area_ids = [row[0] for row in self.env.cr.fetchall()]
            if area_ids:
                self.write({"area_ids": [Command.set(area_ids)]})
            else:
                _logger.info(
                    "No intersecting areas found for incident %s",
                    self.code,
                )
        except psycopg2.Error as e:
            _logger.warning(
                "Failed to link areas from geometry for incident %s: %s",
                self.code,
                e,
            )

Comment on lines +43 to +52
def _column_exists(cr, table, column):
cr.execute(
"""
SELECT 1
FROM information_schema.columns
WHERE table_name = %s AND column_name = %s
""",
(table, column),
)
return bool(cr.fetchone())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When querying information_schema.columns, filtering only by table_name and column_name can lead to false positives if the same table name exists in other schemas (such as pg_catalog or other custom schemas in the database).\n\nTo ensure the check is isolated to the active schema, add AND table_schema = current_schema() to the query.

Suggested change
def _column_exists(cr, table, column):
cr.execute(
"""
SELECT 1
FROM information_schema.columns
WHERE table_name = %s AND column_name = %s
""",
(table, column),
)
return bool(cr.fetchone())
def _column_exists(cr, table, column):
cr.execute(
"""
SELECT 1
FROM information_schema.columns
WHERE table_name = %s AND column_name = %s
AND table_schema = current_schema()
""",
(table, column),
)
return bool(cr.fetchone())

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.36%. Comparing base (bf61488) to head (4bf883d).
⚠️ Report is 1 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_hazard/models/hazard_incident.py 93.43% 9 Missing ⚠️
spp_drims_sl_demo/wizard/drims_demo_generator.py 85.71% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #274      +/-   ##
==========================================
+ Coverage   74.86%   75.36%   +0.50%     
==========================================
  Files        1093      422     -671     
  Lines       63718    25807   -37911     
==========================================
- Hits        47701    19450   -28251     
+ Misses      16017     6357    -9660     
Flag Coverage Δ
endpoint_route_handler ?
spp_aggregation ?
spp_api_v2_change_request 66.41% <ø> (ø)
spp_api_v2_cycles 70.65% <ø> (ø)
spp_api_v2_entitlements 69.96% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products ?
spp_api_v2_programs 92.03% <ø> (ø)
spp_api_v2_service_points ?
spp_api_v2_simulation 71.12% <ø> (ø)
spp_api_v2_vocabulary ?
spp_approval ?
spp_area ?
spp_area_hdx ?
spp_attachment_av_scan ?
spp_audit ?
spp_audit_programs 0.00% <ø> (ø)
spp_banking ?
spp_base_common 90.26% <ø> (ø)
spp_base_setting ?
spp_case_base ?
spp_case_cel ?
spp_case_demo ?
spp_case_entitlements 97.61% <ø> (ø)
spp_case_graduation ?
spp_case_programs 97.14% <ø> (ø)
spp_case_registry ?
spp_case_session ?
spp_cel_domain ?
spp_cel_event ?
spp_cel_registry_search ?
spp_cel_vocabulary ?
spp_change_request_v2 75.46% <ø> (ø)
spp_claim_169 ?
spp_cr_type_assign_program 91.17% <ø> (ø)
spp_cr_types_advanced 0.00% <ø> (ø)
spp_cr_types_base ?
spp_dci ?
spp_dci_client ?
spp_dci_client_dr ?
spp_dci_client_ibr ?
spp_dci_client_sr ?
spp_dci_compliance ?
spp_dci_demo ?
spp_dci_indicators ?
spp_dci_server ?
spp_dci_server_social ?
spp_demo ?
spp_demo_phl_luzon ?
spp_disability_registry ?
spp_drims 81.01% <100.00%> (+0.18%) ⬆️
spp_drims_sl_demo 68.65% <85.71%> (+0.17%) ⬆️
spp_encryption ?
spp_farmer_registry ?
spp_farmer_registry_cr ?
spp_farmer_registry_demo ?
spp_farmer_registry_vocabularies ?
spp_gis ?
spp_gis_report ?
spp_graduation ?
spp_grm ?
spp_grm_case_link ?
spp_grm_demo ?
spp_hazard 97.36% <93.43%> (-2.23%) ⬇️
spp_hazard_programs 97.14% <ø> (ø)
spp_hxl_area ?
spp_import_match ?
spp_indicator ?
spp_irrigation ?
spp_land_record ?
spp_metric ?
spp_metric_service ?
spp_metrics_core ?
spp_metrics_services ?
spp_mis_demo_v2 ?
spp_oauth ?
spp_program_geofence ?
spp_programs 65.27% <ø> (ø)
spp_registrant_gis ?
spp_registry 86.83% <ø> (ø)
spp_registry_group_hierarchy ?
spp_scoring ?
spp_scoring_programs ?
spp_security 66.66% <ø> (ø)
spp_service_points ?
spp_simulation ?
spp_starter_disability_registry ?
spp_starter_farmer_registry ?
spp_starter_social_registry ?
spp_starter_sp_mis ?
spp_statistic ?
spp_storage_backend ?
spp_studio ?
spp_studio_change_requests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_drims/__manifest__.py 0.00% <ø> (ø)
spp_drims/models/hazard_incident_area.py 100.00% <100.00%> (+21.05%) ⬆️
spp_drims_sl_demo/__manifest__.py 0.00% <ø> (ø)
spp_hazard/__manifest__.py 0.00% <ø> (ø)
spp_hazard_programs/__manifest__.py 0.00% <ø> (ø)
spp_drims_sl_demo/wizard/drims_demo_generator.py 68.54% <85.71%> (+0.17%) ⬆️
spp_hazard/models/hazard_incident.py 96.05% <93.43%> (-3.95%) ⬇️

... and 671 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread spp_hazard/migrations/19.0.2.1.0/post-migration.py Fixed
…ion (from #76)

Re-lands the spp_hazard severity->vocabulary change from reverted PR #76,
together with the dependent adaptations in spp_drims, spp_drims_sl_demo and
spp_hazard_programs, plus the two fixes the original PR lacked:

- migrations/19.0.2.1.0/post-migration.py backfills severity_id /
  severity_override_id from the legacy 1-5 Selection columns for databases
  upgrading from v19.0.2.0.x (release Biliran ships the old schema).
  Mapping: 1->minor, 2->moderate, 3->severe, 4->severe, 5->extreme.
- hazard_demo.xml incident-area records now use severity_override_id
  vocabulary refs; the stale severity_override field broke demo installs.

Includes migration regression tests (mapping, area override, idempotency,
unmapped values, fresh-install no-op).
…enerate READMEs

Semgrep flagged f-string SQL in the severity migration; identifiers were
constants but composed SQL via psycopg2.sql removes the pattern entirely.
READMEs regenerated from the updated HISTORY fragments (in-scope modules
only).
Applies CI's pinned-renderer output for spp_hazard README/index.html and
ruff-format's line join in the migration test, taken verbatim from the CI
pre-commit diff.
The flagged query composes identifiers from an in-file constant via
psycopg2.sql.Identifier; no external input reaches it.
Comment thread spp_hazard/migrations/19.0.2.1.0/post-migration.py Fixed
Replaces psycopg2.sql identifier composition with fully literal queries per
target table (only two targets), removing the pattern that pylint-odoo and
semgrep flag. Values remain parameterized.
The local generator env renders RST table widths differently; CI's output
(taken verbatim from its pre-commit diff) is authoritative. Committed without
running local hooks so the README hook cannot rewrite it back.
CI's oca-gen-addon-readme regenerates these five untouched modules on this
PR's merge ref (deterministic target blobs across runs). Content is CI's own
printed diff, applied verbatim; docs-only, no code changes. Committed without
local hooks so the local renderer cannot rewrite it.
- _parse_datetime_string accepts datetime instances (avoids TypeError on
  programmatic values).
- Area-linking query wrapped in a savepoint so an invalid geometry cannot
  leave the transaction aborted for subsequent operations.
- Migration column check scoped to current_schema() to avoid false positives
  from same-named tables in other schemas.
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

All three gemini-code-assist findings applied (commit 4bf883d):

  • _parse_datetime_string accepts datetime instances.
  • Area-linking query wrapped in cr.savepoint() so an invalid geometry cannot leave the transaction aborted.
  • Migration _column_exists scoped to current_schema().

./spp t spp_hazard: 87 passed, 0 failed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants